/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 * 
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

/**
  * A class for holding default information of a tracker
  * 
  * @constructor
  * @param peerIdentity A string which identifies the peer 
  * @param infoHash A string containing the hash information
  * @param port A string representing which port to use
  * @param uploaded A string representing the amount uploaded
  * @param downloaded A string representing the amount downloaded
  * @param left A string representing the amount remaining
  */
DefaultInformation = function(
     peerIdentity
    ,infoHash
    ,port
    ,uploaded
    ,downloaded
    ,left
    ) {
    this["info_hash"] = infoHash;
    this["peer_id"] = peerIdentity;    
    this["port"] = port;
    this["uploaded"] = uploaded;
    this["downloaded"] = downloaded;
    this["left"] = left;
}

DefaultInformation.prototype.classDescription = "Bittorrent Information (Default)";
DefaultInformation.prototype.contractID = "@wikiscraps.com/util/bittorrentInformationDefault;1";
DefaultInformation.prototype.classID = Components.ID("4cfb511b-2de5-4028-8629-fb2848cb6ba1");
DefaultInformation.prototype.QueryInterface = XPCOMUtils.generateQI(
        [Components.interfaces.nsIBittorrentInformation
        ,Components.interfaces.nsISupports]);

/**
 * Gets the property corresponding to a key value
 *
 * @param  key A key corresponding to the property
 * @throws Unknown key: KEY
 * @return The property corresponding to the key
 */
DefaultInformation.prototype.get = function(key) {
    if (this.hasOwnProperty(key)) {
        return this[key];
    }
    throw Components.Exception("Unknown key: " + key);
}

/**
  * A class for handling tracker responses
  * 
  * @constructor
  * @param tracker A tracker  
  */
TrackerResponseHandler = function (tracker) {
    this._tracker = tracker;
}

/**
 * Processes the raw data if the tracker response is successful
 *
 * @param  result The result of the tracker response
 * @throws BTTrackerResponseHandler::success: Tracker.start[internal_func]: error
 */
TrackerResponseHandler.prototype.success = function(result) {
	var e;
	try { 
		this._tracker._processRawData(result); 
	} catch (e) {
        Components.Exception("BTTrackerResponseHandler::success : .");
		reportError("Tracker.start [internal func] : error");
		reportError(e);
	}
}

/**
 * Reports an error if the tracker response is failed
 *
 * @param  result The result of the tracker response
 * @throws BTTracker::start: tracker already started. Use status to check state, and stop to shutdown
 */
TrackerResponseHandler.prototype.failure = function(result) {
    Components.Exception("BTTracker::start : tracker already started. Use status to check state, and stop to shutdown.");
	reportError("Tracker.start [internal func] : error");
	reportError(result);
}

/**
 * A class for handling tracker timeouts
 * 
 * @constructor
 * @param tracker A tracker
 */
TrackerTimeoutHandler = function (tracker) {
    this._tracker = tracker;
}

/**
 * Update the tracker with a new timer
 *
 * @param timer A timer
 */
TrackerTimeoutHandler.prototype.notify = function(timer) {
    this._tracker._updateTracker();
}

/**
  * A class to represent a torrent tracker
  * 
  * @constructor
  * @param string announceURI
  * @param nsIBittorrentInformation informationSource
  */
Tracker = function() {
    this._init = false;   // Whether init has been called successfully. Answers the question 'Are we initialised correctly?'
    this._active = false; // Active if Tracker is running (ie. Tracker request pending or timer active)
    this._info = null;    // Source of torrent & peer information (Should this be split?) Set by init()
    this._announceURI = null;      // URI of tracker. Set by init()
    this._trackerIdentity = null;  // Identity of Tracker (set by initial Tracker response)
    this._minimumInterval = -1;    // Minimum interval (set by last Tracker response)
    this._interval = -1;           // Actual interval (set by user of component, adjusted if invalid when compared to last Tracker response)
    this._timer = Components.classes["@mozilla.org/timer;1"]
       .createInstance(Components.interfaces.nsITimer); // Timer for tracker updates
    this._trackerResponseHandler = new TrackerResponseHandler(this); // Object to handle nsITimer callbacks, avoiding closures.
    this._trackerTimeoutHandler = new TrackerTimeoutHandler(this);   // Object to handle getUrlAsync callbacks, avoiding closures.

    this._distpatchDataList = []; // List of nsIBittorrentTrackerListener to observe update results/errors.
}

/**
 * Initializes a torrent tracker
 *
 * @param announceURI The URI of the tracker
 * @param informationSource Source of the torrent and peer information
 */
Tracker.prototype.init = function(announceURI, informationSource) {
    this._info = informationSource;    // Source of torrent & peer information (Should this be split?)
    this._announceURI = announceURI;   // URI of tracker    
    this._init = true;
}

/**
 * Attempts to start downloading of a torrent
 *
 * @throws BTTracker::start: tracker not initialised.  Call init().
 * @throws BTTracker::start: tracker already started. Use status to check state, and stop to shutdown.
 */
Tracker.prototype.start = function() {
    if (this._init == false) {
        throw Components.Exception("BTTracker::start : tracker not initialised. Call init().");
    }
    // Check it's valid to start.
    // If _timeout is set, we've already started.
    // Therefore, throw exception.
    if (this._active) {
        throw Components.Exception("BTTracker::start : tracker already started. Use status to check state, and stop to shutdown.");
    }
    
    // Make the first request.
    this._urlRequest = this._announceURI + "?" + this._formatParams("started");

    // Get the Tracker's response
    this._getUrlAsync(this._urlRequest, this._trackerResponseHandler);

    this._resetTimer();
}

/**
 * Requests an update from the tracker
 */
Tracker.prototype._updateTracker = function() {
    // Make the update request.
    this._urlRequest = this._announceURI + "?" + this._formatParams();

    // Get the Tracker's response
    this._getUrlAsync(this._urlRequest, this._trackerResponseHandler);
}

/**
 * Reset the timer for a tracker
 */
Tracker.prototype._resetTimer = function() {
    // Prepare to update tracker
    this._active = true;
    this._timer.cancel(); // clear any existing timeout
    if (this._interval > 0) {
        // Reset the timer. If interval not correctly set, expect we will either:
        // a) Not be making a future request
        // b) Will reset the timer later when we know the correct interval, and recall _resetTimer.
        this._timer.initWithCallback(
            this._trackerTimeoutHandler,
            this._interval  * 1000,
            Components.interfaces.nsITimer.TYPE_REPEATING_SLACK);
    }

}

/**
 * Handles formatting of the parameters of a Tracker
 *
 * @param  event An event to signal the formatting of parameters
 * @return A parameterized string
 */
Tracker.prototype._formatParams = function(event) {
    var params = [];

	// Re-encode the info dictionary & hash it to get info_hash
	// http://wiki.theory.org/BitTorrentSpecification#Tracker_Request_Parameters
	// Example: http://tracker.csail.mit.edu:6969/announce?info_hash=%0A%3E%5E03%01%3Ag%C3%81P%7BK%23%C3%B4B%C2%84%06!%C3%9D%09&peer_id=&port=6881&uploaded=0&downloaded=0&left=0&compact=0&event=started&numwant=0

	// escape works. encodeURIComponent doesn't.
	params.push("info_hash=" + escape(this._info.get("info_hash")));
	params.push("peer_id=" + escape(this._info.get("peer_id")));
	params.push("port=" + this._info.get("port"));
	params.push("uploaded=" + this._info.get("uploaded"));
	params.push("downloaded=" + this._info.get("downloaded"));
	params.push("left=" + this._info.get("left"));
	params.push("compact=1"); // If this is missing, we get a 'out of date' response from some trackers 
    // TODO: list trackers that require compact, list trackers that don't care, list trackers that die on compact=1.
	if (event) {
		switch(event) {
		case "started":
		case "stopped":
		case "completed":
			params.push("event=" + event);
		default:
			/* Error, invalid event */
            throw Components.Exception("_formatParams: Unknown event: " + event);
		}
	}
	// params.push("ip=" + encodeURIComponent(this._getModel("local_ip")));
    // We will ask for numwant, but will ignore exception if there isn't one.
    try {
        params.push("numwant=" + this._info.get("numwant"));
    } catch (e) {
        // ignore. Skip requesting a certain amount of peers.
    }
    // If we know the tracker id, re-use it.
	if (this._trackerIdentity != null) {
		params.push("trackerid=" + encodeURIComponent(this._trackerIdentity));
	}
	return params.join("&");    
}	

/**
 * Stops a tracker
 */
Tracker.prototype.stop = function() {
    if (this._active == false) {
        // error
        // Not fatal, and the resulting state will be as the caller expects (ie. stopped)
        // so don't throw exception.
        reportWarning("Tracker.stop : call to stop when tracker not running");
    } else {
        this._active = false;
        this._timer.cancel();
    }    
}

/**
 * Retrieves data from a tracker
 *
 * @param data The data to be dispatch
 * @throws BTTracker::_dispatchData: Dispatched data failure (observer threw exception)
 */
Tracker.prototype._dispatchData = function(data) {
    var i;
    var e;

    try {
        for (i=0; i < this._distpatchDataList.length; i++) {
            this._distpatchDataList[i].update(this,data);
        }
    } catch (e) {
        throw Components.Exception("BTTracker::_dispatchData : Dispatched data failure (observer threw exception)");
    }
}

/**
 * Adds a listener for trackers
 *
 * @param listener A listener
 */
Tracker.prototype.addTrackerListener = function (listener) {
    this._distpatchDataList.push(listener);
}

/**
 * Removes a tracker listener
 *
 * @param listener A listener
 */
Tracker.prototype.removeTrackerListener = function (listener) {
    var p = this._distpatchDataList.indexOf(listener);
    if (p != -1) {
        // Splice is the one that REMOVES items from an array (and optionally inserts)
        this._distpatchDataList.splice(p, 1);
    }
}

/**
 * Process the data provided by a tracker
 *
 * @param data The data to be processed
 * @throws Tracker::_processRawData: Data failed to decode correctly
 */
Tracker.prototype._processData = function(data) {
    var decoded;
    var e;

    // See http://wiki.theory.org/BitTorrentSpecification#Tracker_Response
    try {
		decoded = bencoding.bdecode(data);
	} catch (e) {
		reportError("Tracker _processRawData : Data failed to decode correctly");
		throw new BadResponseException();
	}

	if (decoded["tracker id"]) {
		this._trackerid = decoded["tracker id"];
	}

    if (decoded["interval"]) {
        if (this._interval == -1) {
            this._interval = decoded["interval"];
            this._resetTimer();
        }		
	}

    // Interpret Tracker client/server relevant info
	if (decoded["min interval"]) {
		// This will likely suck horribly. Reset timer if interval < min interval.
		this._minimumInterval = decoded["min interval"];
        if (this._interval < this._minimumInterval) {
            this._interval = this._minimumInterval;
            this._resetTimer();
        }
	}

    // Update listeners
    this._dispatchData(decoded);
}

/**
 * An asynchronous GET method for a tracker
 *
 * @param url URL for a tracker
 * @param callback A callback to the tracker
 * @throws getUrlAsync: error for URL
 * @throws BadCodeException
 */
Tracker.prototype._getUrlAsync = function getUrlAsync(url, callback) {
	var ioserv = Components.classes["@mozilla.org/network/io-service;1"] 
		.getService(Components.interfaces.nsIIOService); 
	var channel = ioserv.newChannel(url, 0, null); 
    var instream;
	var result = "";
	var wrappedInstream = false;
	var observer = {

		onStartRequest: function(request, context){
			
	  
		},
		onStopRequest: function(request, context, status){
			if (wrappedInstream != false) {
				wrappedInstream.close();
			}
			var e;
            wrappedInstream = null;
            observer = null;
            instream = null;
            channel = null;
			try {
				callback(result);			
			} catch(e) {
				reportError("getUrlAsync: error for " + url);
				reportError(e);
				reportError(typeof(e));
				//if (typeof(e) == "object") {
					var i;
					for (i in e) {
						log(" " + i + ":" + e[i]);
					}
				//}
                result = null;
				throw new BadCodeException();				
			}
            result = null;
		},
		onDataAvailable: function(request, context, inputStream, offset, count){
			// This heavy manner is from https://developer.mozilla.org/En/Creating_Sandboxed_HTTP_Connections
			if (wrappedInstream == false) {
				wrappedInstream = Components.classes["@mozilla.org/binaryinputstream;1"]
					.createInstance(Components.interfaces.nsIBinaryInputStream);
				wrappedInstream.setInputStream(inputStream);
			}
			result = result + wrappedInstream.readBytes(count);        
		}	
	};

	instream = channel.asyncOpen(observer, null); 
}



/**
 * A class for handling bittorrent downloads in XULRunner Apps
 *
 * @constructor
 */
BTTracker = function() {
    this._tracker = new Tracker();
};

BTTracker.prototype = {
    classDescription : "Bittorrent Tracker Client",
    contractID : "@wikiscraps.com/comm/trackerClient;1",
    classID : Components.ID("6cf60070-4664-4dd7-bbb3-d2d28f7f0125"),
    QueryInterface : XPCOMUtils.generateQI(
        [Components.interfaces.nsIBittorrentTracker
        ,Components.interfaces.nsISupports]),
/**
 * Initializes a torrent tracker
 *
 * @param announceURI URI of the tracker
 * @param infoSource Source information of the tracker
 */
    init : function(announceURI, infoSource) { this._tracker.init(announceURI, infoSource); },

/**
 * Starts the torrent tracker
 */
    start : function() { this._tracker.start(); },

/**
 * Stops the torrent tracker
 */
    stop : function() { this._tracker.stop(); },

/**
 * Adds a tracker listener
 *
 * @param listener A listener to be added
 */
    addTrackerListener : function(listener) { this._tracker.addTrackerListener(listener); },

/**
 * Removes a tracker listener
 *
 * @param listener A listener to be removed
 */
    removeTrackerListener : function(listener) { this._tracker.removeTrackerListener(listener); }

}

var NSGetModule = XPCOMUtils.generateNSGetModule([BTTracker,DefaultInformation]);


